import type {
  AnalyzeOptions,
  ScopeManager,
  Variable,
} from '@typescript-eslint/scope-manager';
import type { TSESTree } from '@typescript-eslint/types';
import type { TSESTreeOptions } from '@typescript-eslint/typescript-estree';

import { analyze } from '@typescript-eslint/scope-manager';
import { AST_TOKEN_TYPES } from '@typescript-eslint/types';
import { parse } from '@typescript-eslint/typescript-estree';
import { FlatESLint } from '@typescript-eslint/utils/ts-eslint';
import fs from 'node:fs';
import path from 'node:path';
import prettier from 'prettier';
import { rimraf } from 'rimraf';
import * as ts from 'typescript';

import {
  PACKAGES_SCOPE_MANAGER,
  PACKAGES_TYPES,
  PRETTIER_CONFIG_PATH,
  REPO_ROOT,
} from './paths.mts';

function parseAndAnalyze(
  code: string,
  analyzeOptions: AnalyzeOptions,
  parserOptions: TSESTreeOptions,
): {
  ast: ReturnType<typeof parse>;
  scopeManager: ReturnType<typeof analyze>;
} {
  const ast = parse(code, { ...parserOptions });
  const scopeManager = analyze(ast, analyzeOptions);

  return { ast, scopeManager };
}

// @ts-expect-error - libMap is private and not typed
const tsLibMap = ts.libMap as Map<string, string>;
const libMap = new Map(tsLibMap);
// add the "full" libs as well - these are used by the default config resolution system
for (const [lib] of tsLibMap) {
  if (
    (/^es2\d{3}$/.test(lib) || lib === 'esnext') &&
    // there's no "full" lib for es2015
    lib !== 'es2015'
  ) {
    libMap.set(`${lib}.full`, `lib.${lib}.full.d.ts`);
  }
}
// the base lib used when the target is unknown
libMap.set('lib', 'lib.d.ts');

function addAutoGeneratedComment(code: string[]): string {
  return [
    '// THIS CODE WAS AUTOMATICALLY GENERATED',
    '// DO NOT EDIT THIS CODE BY HAND',
    '// RUN THE FOLLOWING COMMAND FROM THE WORKSPACE ROOT TO REGENERATE:',
    '// npx nx generate-lib repo',
    '',
    ...code,
  ].join('\n');
}

const PRETTIER_CONFIG = prettier.resolveConfig('file.ts', {
  config: PRETTIER_CONFIG_PATH,
});
const TS_LIB_FOLDER = path.join(REPO_ROOT, 'node_modules', 'typescript', 'lib');
const OUTPUT_FOLDER = path.join(PACKAGES_SCOPE_MANAGER, 'src', 'lib');
const TYPES_FILE = path.join(PACKAGES_TYPES, 'src', 'lib.ts');
const BARREL_PATH = path.join(OUTPUT_FOLDER, 'index.ts');

const BASE_CONFIG_MODULE_NAME = 'base-config';
const SHARED_CONFIG_MODULE = path.join(
  OUTPUT_FOLDER,
  `${BASE_CONFIG_MODULE_NAME}.ts`,
);
type BASE_CONFIG_EXPORT_NAMES = 'TYPE' | 'TYPE_VALUE' | 'VALUE';

async function formatCode(code: string[]): Promise<string> {
  return await prettier.format(addAutoGeneratedComment(code), {
    parser: 'typescript',
    ...(await PRETTIER_CONFIG),
  });
}

function sanitize(name: string): string {
  return name.replaceAll('.', '_');
}

function getVariablesFromScope(scopeManager: ScopeManager): Variable[] {
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
  const scope = scopeManager.globalScope!.childScopes[0];
  const variables: Variable[] = [];
  for (const variable of scope.variables) {
    if (variable.isTypeVariable) {
      variables.push(variable);
    }
  }

  return variables;
}

const REFERENCE_REGEX = /\/ <reference lib="(.+)" \/>/;
function getReferences(
  ast: { comments: TSESTree.Comment[] } & TSESTree.Program,
): Set<string> {
  const comments = ast.comments.filter(
    c =>
      c.type === AST_TOKEN_TYPES.Line &&
      c.value.startsWith('/ <reference lib="'),
  );

  const references = new Set<string>();
  for (const comment of comments) {
    const match = REFERENCE_REGEX.exec(comment.value);
    if (!match) {
      continue;
    }

    references.add(match[1]);
  }
  return references;
}

try {
  rimraf.sync(OUTPUT_FOLDER);
} catch {
  // ignored
}
try {
  fs.mkdirSync(OUTPUT_FOLDER);
} catch {
  // ignored
}

const filesWritten: string[] = [SHARED_CONFIG_MODULE, TYPES_FILE, BARREL_PATH];

// the shared
fs.writeFileSync(
  SHARED_CONFIG_MODULE,
  await formatCode([
    `export const TYPE = Object.freeze(${JSON.stringify({
      eslintImplicitGlobalSetting: 'readonly',
      isTypeVariable: true,
      isValueVariable: false,
    })});`,
    `export const VALUE = Object.freeze(${JSON.stringify({
      eslintImplicitGlobalSetting: 'readonly',
      isTypeVariable: false,
      isValueVariable: true,
    })});`,
    `export const TYPE_VALUE = Object.freeze(${JSON.stringify({
      eslintImplicitGlobalSetting: 'readonly',
      isTypeVariable: true,
      isValueVariable: true,
    })});`,
    '',
  ]),
);

for (const [libName, filename] of libMap) {
  const libPath = path.join(TS_LIB_FOLDER, filename);
  const { ast, scopeManager } = parseAndAnalyze(
    fs.readFileSync(libPath, 'utf8'),
    {
      // we don't want any libs
      lib: [],
      sourceType: 'module',
    },
    {
      comment: true,
      loc: true,
      range: true,
    },
  ) as {
    // https://github.com/typescript-eslint/typescript-eslint/issues/8347
    ast: { comments: TSESTree.Comment[] } & TSESTree.Program;
  } & ReturnType<typeof parseAndAnalyze>;

  const code = [`export const ${sanitize(libName)}: LibDefinition = {`];

  const references = getReferences(ast);
  if (references.size > 0) {
    // add a newline before the export
    code.unshift('');
  }

  // import and spread all of the references
  const imports = ["import type { LibDefinition } from '../variable';"];
  code.push('libs: [');
  for (const reference of references) {
    const name = sanitize(reference);
    imports.push(`import { ${name} } from './${reference}'`);
    code.push(`${name},`);
  }
  code.push('],');

  const requiredBaseImports = new Set<BASE_CONFIG_EXPORT_NAMES>();

  // add a declaration for each variable
  const variables = getVariablesFromScope(scopeManager);
  code.push('variables: [');
  for (const variable of variables) {
    const importName = ((): BASE_CONFIG_EXPORT_NAMES => {
      if (variable.isTypeVariable && variable.isValueVariable) {
        return 'TYPE_VALUE';
      }
      if (variable.isTypeVariable) {
        return 'TYPE';
      }
      if (variable.isValueVariable) {
        return 'VALUE';
      }
      // shouldn't happen
      throw new Error(
        "Unexpected variable that's is not a type or value variable",
      );
    })();
    requiredBaseImports.add(importName);

    code.push(`['${variable.name}', ${importName}],`);
  }
  code.push('],');
  code.push('};');

  if (requiredBaseImports.size > 0) {
    imports.push(
      `import {${[...requiredBaseImports]
        .sort()
        .join(',')}} from './${BASE_CONFIG_MODULE_NAME}';`,
    );
  }

  if (imports.length > 0) {
    code.unshift(...imports, '');
  }

  const formattedCode = await formatCode(code);
  const writePath = path.join(OUTPUT_FOLDER, `${libName}.ts`);
  fs.writeFileSync(writePath, formattedCode);
  filesWritten.push(writePath);

  console.log(
    'Wrote',
    variables.length,
    'variables, and',
    references.size,
    'references for',
    libName,
  );
}

// generate and write a barrel file
const barrelImports = []; // use a separate way so everything is in the same order
const barrelCode = [
  '',
  `export const lib: ReadonlyMap<string, LibDefinition> =`,
  `new Map<string, LibDefinition>([`,
];
// Call `Object.entries` during barrel construction to avoid redundantly calling
// and allocating a new array on every reference
for (const lib of libMap.keys()) {
  const name = sanitize(lib);
  if (name === 'lib') {
    barrelImports.push(`import { lib as libBase } from './${lib}'`);
    barrelCode.push(`['${lib}', libBase],`);
  } else {
    barrelImports.push(`import { ${name} } from './${lib}'`);
    barrelCode.push(`['${lib}', ${name}],`);
  }
}
barrelCode.unshift(...barrelImports);
barrelCode.unshift('', `import type { LibDefinition } from '../variable';`);
barrelCode.push(']);');

const formattedBarrelCode = await formatCode(barrelCode);

fs.writeFileSync(BARREL_PATH, formattedBarrelCode);
console.log('Wrote barrel file');

// generate a string union type for the lib names

const libUnionCode = [
  `export type Lib = ${[...libMap.keys()].map(k => `'${k}'`).join(' | ')};`,
];
const formattedLibUnionCode = await formatCode(libUnionCode);

fs.writeFileSync(TYPES_FILE, formattedLibUnionCode);
console.log('Wrote Lib union type file');

const lint = new FlatESLint({
  fix: true,
});
const results = await lint.lintFiles(filesWritten);
await FlatESLint.outputFixes(results);
console.log('Autofixed lint errors');
